﻿#region Using References

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Drawing;
using System.IO;
using Beaker.OpenCube.Game;
using Beaker.Services.FileSystem;
using Ionic.Zip;

#endregion

namespace Beaker.OpenCube.Textures
{
	/// <summary>
	/// Reader for reading texture pack files
	/// </summary>
	public sealed class TexturePackReader
	{
		private readonly ZipFile _zipFile;
		private readonly ZipFile _fallBackZipFile;
		private readonly IImageCache _imageCache;

		public TexturePackReader( Stream stream )
			: this( stream, null, null )
		{
		}

		public TexturePackReader( Stream stream, IImageCache imageCache )
			: this( stream, null, imageCache )
		{
		}

		public TexturePackReader( Stream stream, Stream fallBackStream )
			: this( stream, fallBackStream, null )
		{
		}

		public TexturePackReader( Stream stream, Stream fallBackStream, IImageCache imageCache )
		{
			Contract.Requires<ArgumentNullException>( stream != null );
			Contract.Requires<ArgumentException>( stream.CanRead );
			Contract.Requires<ArgumentException>( stream.CanSeek );
			Contract.Requires<ArgumentException>( fallBackStream == null || fallBackStream.CanRead );
			Contract.Requires<ArgumentException>( fallBackStream == null || fallBackStream.CanSeek );

			try
			{
				_zipFile = ZipFile.Read( stream );
			}
			catch( ZipException innerException )
			{
				throw new FormatException( "Not a (valid) zip file", innerException );
			}

			if( fallBackStream != null )
			{
				try
				{
					_fallBackZipFile = ZipFile.Read( fallBackStream );
				}
				catch( ZipException innerException )
				{
					throw new FormatException( "Fallback not a (valid) zip file", innerException );
				}
			}

			_imageCache = imageCache;
		}

		public string ReadName()
		{
			ZipEntry entry = _zipFile["pack.txt"];
			if( entry == null )
				return string.Empty;

			using( var nameStream = new MemoryStream() )
			{
				// extract frame data
				entry.Extract( nameStream );

				// flush and move back to start
				nameStream.Flush();
				nameStream.Position = 0;

				// now read as a single string
				var nameReader = new StreamReader( nameStream );
				return nameReader.ReadLine();
			}
		}

		/// <summary>
		/// Get the image from the cache, or read it from disk
		/// </summary>
		/// <param name="relativePath">The relative path of the image to get.</param>
		/// <returns>
		/// The image, or <c>null</c> when no image could be found
		/// </returns>
		private Bitmap GetOrReadImage( string relativePath )
		{
			Bitmap bitmap = null;

			if( _imageCache != null )
				bitmap = _imageCache.GetImage( relativePath );

			if( bitmap == null )
			{
				bitmap = TryReadImage( relativePath );
				if( bitmap != null && _imageCache != null )
					_imageCache.AddImage( relativePath, bitmap );
			}

			return bitmap;
		}

		/// <summary>
		/// Reads the image with the given relative path. Will not use cached images.
		/// </summary>
		/// <param name="relativePath">The relative path and filename of the image.</param>
		/// <returns>
		/// The loaded image
		/// </returns>
		/// <exception cref="System.IO.FileNotFoundException">Thrown when the file is not available</exception>
		public Bitmap ReadImage( string relativePath )
		{
			Contract.Requires<ArgumentNullException>( !string.IsNullOrEmpty( relativePath ) );
			Contract.Ensures( Contract.Result<Bitmap>() != null );

			Bitmap bitmap = TryReadImage( relativePath );

			if( bitmap == null )
				throw new FileNotFoundException();

			return bitmap;
		}

		/// <summary>
		/// Tries to read the image with the given relative path. Will not use cached images.
		/// </summary>
		/// <param name="relativePath">The relative path of the image to read.</param>
		/// <returns>
		/// The loaded image or <c>null</c> when it could not be read
		/// </returns>
		public Bitmap TryReadImage( string relativePath )
		{
			Contract.Requires<ArgumentNullException>( !string.IsNullOrEmpty( relativePath ) );

			Bitmap bitmap = TryReadImageFromZip( _zipFile, relativePath );
			if( bitmap == null && _fallBackZipFile != null )
				bitmap = TryReadImageFromZip( _fallBackZipFile, relativePath );

			return bitmap;
		}

		/// <summary>
		/// Tries the read image from zip the given zip file
		/// </summary>
		/// <param name="zip">The zip file to read the image from.</param>
		/// <param name="relativePath">The relative path of the image to read.</param>
		/// <returns>
		/// The loaded image or <c>null</c> when it could not be read
		/// </returns>
		private Bitmap TryReadImageFromZip( ZipFile zip, string relativePath )
		{
			Contract.Requires<ArgumentNullException>( zip != null );
			Contract.Requires<ArgumentNullException>( !string.IsNullOrEmpty( relativePath ) );

			// now open the file
			using( var imageStream = new MemoryStream() )
			{
				ZipEntry imageEntry = zip[relativePath];
				if( imageEntry == null )
					return null;

				imageEntry.Extract( imageStream );

				// flush and move back to start
				imageStream.Flush();
				imageStream.Position = 0;

				// load the image from memory stream
				return (Bitmap)Image.FromStream( imageStream );
			}
		}

		/// <summary>
		/// Reads all available block textures from the stream
		/// </summary>
		/// <returns>
		/// A list containing all the available block textures in the stream.
		/// </returns>
		public IReadOnlyList<BlockTexture> ReadAllAvailableBlockTextures()
		{
			var textures = new List<BlockTexture>();

			foreach( Block block in BlockTextureMapping.GetSupportedBlockTypes() )
			{
				BlockTexture texture = ReadBlockTexture( block );
				if( texture != null )
					textures.Add( texture );
			}

			return new ReadOnlyCollection<BlockTexture>( textures );
		}

		/// <summary>
		/// Reads the texture for the given block type
		/// </summary>
		/// <param name="block">Type of the block to read the texture for.</param>
		/// <returns>
		/// The texture for the requested block or <c>null</c> when it could not be read.
		/// </returns>
		public BlockTexture ReadBlockTexture( Block block )
		{
			// try get the name for the block
			IReadOnlyCollection<Tuple<BlockTextureFlags, string>> mappings = BlockTextureMapping.GetTextureMapping( block );
			if( mappings == null )
				return null;

			var textures = new List<Tuple<BlockTextureFlags, Texture>>();
			foreach( Tuple<BlockTextureFlags, string> mapping in mappings )
			{
				Texture texture = ReadTexture( mapping.Item2 );
				if( texture != null )
					textures.Add( Tuple.Create( mapping.Item1, texture ) );
			}

			// no textures found, return null
			if( textures.Count == 0 )
				return null;

			return new BlockTexture( block, textures );
		}

		/// <summary>
		/// Reads the texture with the given name.
		/// </summary>
		/// <param name="name">The name of the texture to read.</param>
		/// <returns>The texture read or <c>null</c> when it could not be loaded.</returns>
		private Texture ReadTexture( string name )
		{
			Contract.Requires<ArgumentNullException>( !string.IsNullOrEmpty( name ) );

			// get the image
			Bitmap image = GetOrReadImage( "textures/blocks/" + name + ".png" );

			// when not found, abort and return null
			if( image == null )
				return null;

			// now check if there is a txt file with frame information
			IEnumerable<FrameInfo> frameInfo = null;
			ZipEntry frameEntry = _zipFile["textures/blocks/" + name + ".txt"];
			if( frameEntry != null )
				frameInfo = ReadFrameInfo( frameEntry );

			// load the texture from memory stream
			return new Texture( image, frameInfo );
		}

		/// <summary>
		/// Reads the frame info.
		/// </summary>
		/// <param name="frameZipEntry">The zip entry to the file with the frame information.</param>
		/// <returns>
		/// A sequence with the frame information.
		/// </returns>
		/// <exception cref="System.FormatException">Thrown when the frame info could not be parsed.</exception>
		private static IEnumerable<FrameInfo> ReadFrameInfo( ZipEntry frameZipEntry )
		{
			IEnumerable<FrameInfo> frameInfo;

			using( var frameStream = new MemoryStream() )
			{
				// extract frame data
				frameZipEntry.Extract( frameStream );

				// flush and move back to start
				frameStream.Flush();
				frameStream.Position = 0;

				// now read as a single string
				var frameReader = new StreamReader( frameStream );
				string frameInformation = frameReader.ReadToEnd();
				if( !FrameInfo.TryParse( frameInformation, out frameInfo ) )
					throw new FormatException( "Frame info was invalid" );
			}

			return frameInfo;
		}


		/// <summary>
		/// Opens the texture pack from the given stream
		/// </summary>
		/// <param name="stream">The stream to open the texture pack from.</param>
		/// <param name="fallBackStream">The fall back stream to use - usually the game jar - when a texture is not available in the main stream.</param>
		/// <param name="imageCache">The image cache.</param>
		/// <returns></returns>
		public static TexturePackReader FromStream( Stream stream, Stream fallBackStream = null, IImageCache imageCache = null )
		{
			Contract.Requires<ArgumentNullException>( stream != null );
			Contract.Requires<ArgumentException>( stream.CanRead, "Stream must be readable" );
			Contract.Requires<ArgumentException>( stream.CanSeek, "Stream must be seekable" );
			Contract.Requires<ArgumentException>( fallBackStream == null || fallBackStream.CanRead, "Fallback stream must be null, or readable" );
			Contract.Requires<ArgumentException>( fallBackStream == null || fallBackStream.CanSeek, "Fallback stream must be null, or seekable" );
			Contract.Ensures( Contract.Result<TexturePackReader>() != null );

			return new TexturePackReader( stream, fallBackStream, imageCache );
		}

		/// <summary>
		/// Opens the texture pack from the given file
		/// </summary>
		/// <param name="file">The file to opne the texture pack from.</param>
		/// <param name="fallBackFile">The fall back file to use - usually the game jar - when a texture is not available in the main file.</param>
		/// <param name="imageCache">The image cache.</param>
		/// <returns></returns>
		public static TexturePackReader FromFile( IFile file, IFile fallBackFile = null, IImageCache imageCache = null )
		{
			return FromStream( file.OpenRead(), fallBackFile != null ? fallBackFile.OpenRead() : null );
		}

	}
}
